Electron 快速应用

最近使用 Electron 开发了一款知识管理客户端应用 - 布丁笔记,主要功能是用来管理自己的私人笔记和公开博客,支持一键上传 github,亦可同步 github issues

当然,关于布丁笔记如何实现的,我会在后续的文章中更新,今天主要是来聊一聊如何使用 Electron 快速开发一款客户端应用

介绍

正如我们使用的 web 技术一样,Electron 也是使用 JavascriptHTMLCSS 来快速简便地搭建一个跨平台的客户端应用

具体来说,Electron 是通过将 ChromiumNode.js 合并到同一个运行时环境中,并将其打包为 Mac,Windows 和 Linux 系统下的应用来实现这一目的的

所以,它可以利用 Chromium 的浏览器引擎来完成 UI 渲染,也可以利用 Node.js 实现服务端 JS 编程并可以直接操作文件和调用系统 API,甚至操作数据库,同时,还可以使用 Node 提供的 request 模块进行网络请求,无需考虑跨域

运行原理

Chromium

相当于 Chrome 的实验版,新功能会先在 Chromium 上验证,后续才应用在 Chrome 上

Node.js

Node.js 是非常强大的,既可以写后台的 CRUD,又可以做中间件,现在又可以写前端

Electron 中,主要运用其 PathfsCrypto 等模块,操作系统底层 API

系统API

内置原生应用程序接口,以支持 GUI,调用系统通知、打开系统文件夹等

主进程

Electron 有两种进程:主进程和渲染进程,两者之间可以进行进程间通信

主进程可以创建多个渲染进程;同时控制整个应用生命周期,包括启动、退出 APP 以及对 APP 做一些监听;主要用来处理原生应用逻辑,调用系统底层功能,可调用 Node API、Electron 主进程 API

一个 Electron 应用有且只有一个主进程,对应为 package.jsonmain 脚本的进程

渲染进程

渲染进程负责界面渲染,做一些界面交互,可以创建浏览器窗口,也可以调用 Node APIDOM APIElectron 渲染进程 API

不妨把渲染进程想像成一个浏览器窗口,它能存在多个并且相互独立,不同的是,它能调用 Node API

基础

生命周期

通过主进程的 app 模块控制整个应用的生命周期

比如当初始化时完成时触发 ready 事件

app.on('ready', () => {
  // 创建窗口、加载页面等操作
  createWindow()
})

当所有的窗口都关闭时会触发 window-all-closed 事件

app.on('window-all-closed', () => {
  if(process.platform !== 'darwin'){
      app.quit();     // 退出应用
  }
})

窗口

Electron 提供了一个 BrowserWindow 模块用于创建和控制浏览器窗口

通过关键字 new 实例化返回 win 对象

win = new BrowserWindow({
  width: 1100,         // 窗口宽度
  height: 800,        // 窗口高度
  minWidth: 900,
  minHeight: 600,
  title: '布丁笔记',
  fullscreen: false,  // 不允许全屏
  resizable: false    // 不允许改变窗口size,不然布局就乱了啊
})

窗口创建完是一片空白的,还需要通过 win.loadURL() 来加载要显示的页面

win.loadURL('app://./index.html')

shell 模块

使用系统默认应用管理文件和 URL,在主进程和渲染进程中都可以用到该模块

import { shell } from 'electron'

shell.moveItemToTrash(fullPath)   // 将文件删除至电脑回收站
shell.openExternal(`file://${postPath}.html`)    // 在默认浏览器中打开URL

打包应用

electron-builder 是基于 electron-packager 打包出来的程序再做安装处理,将项目打包成安装文件

npm install electron-builder -S    // 安装

electron-builder --win             // 打包

进程通信

渲染进程如果想进行原生的 GUI 操作,必须和主进程通讯,请求主进程来完成这些操作

在讲具体的通信过程前,我们先了解下 EventEmitter 类,它是 NodeJS 事件的基础,由 NodeJS 中的 events 模块导出

EventEmitter 的核心就是事件触发与事件监听器功能的封装。它实现了事件模型需要的接口,包括 addListenerremoveListener, emit 及其它工具方法. 同原生 JavaScript 事件类似, 采用了发布/订阅(观察者)的方式, 使用内部 _events 列表来记录注册的事件处理器

接下来提到的 ipcRendereripcMainonsend 进行监听和发送消息都是 EventEmitter 定义的相关接口

渲染进程向主进程通信

ipcRenderer 是一个 EventEmitter 的实例,可以使用其 sendsendSync 方法发送异步或同步的消息到主进程,同时可以接收主进程回复的消息

// 在渲染进程引入ipcRenderer
import { ipcRenderer } from 'electron'
ipcRenderer.send('sync-render', '我是来自渲染进程的异步消息')
const msg = ipcRenderer.sendSync('async-render', '我是来自渲染进程的同步消息')

注意:发送同步消息将会阻塞整个渲染进程,直到收到主进程的响应。所以我们在实际项目开发中应该尽可能的使用异步通信

ipcMain 模块也是 EventEmitter 类的一个实例,用于监听渲染进程传过来的消息并处理

ipcMain.on('sync-render', (event, data) => {
  console.log(data);
})

主进程向渲染进程通信

webContents 是一个事件发出者,它负责渲染并控制网页,也是 BrowserWindow 对象的属性

ipcMain 中的 event.sender,返回发送消息的 webContents 对象,所以包含着 send() 方法用于发送消息

ipcMain 接受消息的回调函数中,我们可以回应消息到渲染进程

ipcMain.on('sync-render', (event, data) => {
  console.log(data);
  event.sender.send('main-msg', '主进程收到了渲染进程的【异步】消息!')
})

渲染进程通过 ipcRenderer.on 监听

ipcRenderer.on('main-msg', (event, msg) => {
  console.log(msg);
})

渲染进程间通信

Electron 并没有提供渲染进程之间相互通信的方式,我们可以在主进程中建立一个消息中转站

渲染进程之间通信首先发送消息到主进程,主进程的中转站接收到消息后根据条件进行分发

remote

remote 模块为渲染进程和主进程通信提供了一种简单方法

Electron 中,有许多模块只存在主进程中,想要调用这些模块的方法需要通过 ipc 模块向主进程发送消息,让主进程调用这些方法,而使用 remote 模块,可以在渲染进程中调用这些只存在于主进程对象的方法

import { remote } from 'electron'

const BrowserWindow = remote.BrowserWindow      // 访问主进程中的BrowserWindow模块

let win = new BrowserWindow()                  // 其他的跟主进程的操作都一样

win.setRepresentedFilename(this.post.localPath)
win.setDocumentEdited(true)
win.setTitle('布丁笔记')

数据持久存储

数据持久化有很多种方案,因为是前端浏览器,我们可以选择 localStorage, CookieindexDB 等等

考虑可靠性,持久化以及存储空间,还可以通过 Electron 写文件的方式,把数据写入到应用路径下

这样即使 app 被卸载了,只要数据没被清空,用户数据还在

通过 Electron app getPath 可以获得应用存储路径

import { app } from 'electron'
app.getPath('userData')

而对于文件是否写入的判断,以及进程间的数据共享,这里我推荐使用开源库 lowdb,以 key-value 的格式存储 json 文件

lowdb 是一个由 Lodash 支持的小型本地 JSON 数据库(支持 NodeElectron 和浏览器)

总结

  • Electron = Node.js + 谷歌浏览器 + 平常的 JS 代码生成的应用,最终打包成安装包,就是一个完整的客户端应用
  • Electron 分两个进程,主进程负责原生应用逻辑的创建以及底层功能的调用,渲染进程负责 UI 界面展示
  • 两个进程间是通过 iPCRenderiPCMain 方法,以及 remote 模块通信,前者是是基于发布订阅机制,自定义事件的监听和触发进行进程间通信;后者类似于挂载在全局的属性上进行通信
  • Electron 相当于给 React 或者 Vue 生成的单页面应用套了一层壳,如果涉及到文件操作这类的复杂功能,就要依靠 Electron 的主进程,因为主进程可以直接调用 Node.jsAPI,还可以调用系统底层功能

参考